﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.Reflection;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.ComponentModel;

namespace DCMS_Framework {
  public abstract class UndoableBase {

    [NotUndoable( )]
    private Stack<byte[ ]> stateStack = new Stack<byte[ ]>( );


    protected UndoableBase( ) { }

    [EditorBrowsable(EditorBrowsableState.Never)]
    protected int EditLevel {
      get { return stateStack.Count; }
    }

    [EditorBrowsable( EditorBrowsableState.Advanced )]
    protected virtual void CopyStateComplete( ) { }

    /// <summary>
    /// Copies the state of the object and places the copy
    /// onto the state stack.
    /// </summary>
    [EditorBrowsable( EditorBrowsableState.Never )]
    protected internal void CopyState( ) {
      Type currentType = this.GetType( );
      HybridDictionary state = new HybridDictionary( );
      FieldInfo[ ] fields = null;

      do {
        // get the list of fields in this type
        fields = currentType.GetFields( BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public );
        foreach ( FieldInfo field in fields ) {
          // make sure we process only our variables
          if ( object.ReferenceEquals( field.DeclaringType, currentType ) ) {
            // see if this field is marked as not undoable
            if ( !NotUndoableField( field ) ) {
              // the field is undoable, so it needs to be processed.
              object value = field.GetValue( this );
              if ( typeof( UndoableBase ).IsAssignableFrom( field.FieldType ) ) {
                // make sure the variable has a value
                if ( value == null ) {
                  // variable has no value - store that fact
                  state.Add( GetFieldName( field ), value );
                } else {
                  // this is a child object, cascade the call
                  ( ( UndoableBase ) value ).CopyState( );
                }
              } else {
                // this is a normal field, simply trap the value
                state.Add( GetFieldName( field ), value );
              }
            }
          }
        }
        currentType = currentType.BaseType;

      } while ( !( object.ReferenceEquals( currentType, typeof( UndoableBase ) ) ) );

      // serialize the state and stack it
      using ( MemoryStream bufferStream = new MemoryStream( ) ) {
        BinaryFormatter formatter = new BinaryFormatter( );
        formatter.Serialize( bufferStream, state );
        stateStack.Push( bufferStream.ToArray( ) );
      }
      CopyStateComplete( );
    }

    [EditorBrowsable( EditorBrowsableState.Advanced )]
    protected virtual void UndoChangesComplete( ) { }

    [EditorBrowsable( EditorBrowsableState.Never )]
    protected internal void UndoChanges( ) {
      // if we are a child object we might be asked to
      // undo below the level where stacked states,
      // so just do nothing in that case
      if ( this.EditLevel > 0 ) {
        HybridDictionary state;
        using ( MemoryStream bufferStream = new MemoryStream( stateStack.Pop( ) ) ) {
          bufferStream.Position = 0;
          BinaryFormatter formatter = new BinaryFormatter( );
          state = ( HybridDictionary ) formatter.Deserialize( bufferStream );
        }
        Type currentType = this.GetType( );
        FieldInfo[ ] fields;
        do {
          // get the list of fields in this type
          fields = currentType.GetFields( BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public );
          foreach ( FieldInfo field in fields ) {
            // make sure we process only our variables
            if ( field.DeclaringType == currentType ) {
              // see if the field is undoable or not
              if ( !NotUndoableField( field ) ) {
                // the field is undoable, so restore its value
                object value = field.GetValue( this );
                if ( typeof( UndoableBase ).IsAssignableFrom( field.FieldType ) ) {
                  // this is a child object
                  // see if the previous value was empty
                  if ( state.Contains( GetFieldName( field ) ) ) {
                    // previous value was empty - restore to empty
                    field.SetValue( this, null );
                  } else {
                    // make sure the variable has a value
                    if ( value != null ) {
                      // this is a child object, cascade the call.
                      ( ( UndoableBase ) value ).UndoChanges( );
                    }
                  }
                } else {
                  // this is a regular field, restore its value
                  field.SetValue( this, state[ GetFieldName( field ) ] );
                }
              }
            }
          }
          currentType = currentType.BaseType;
        } while ( !( object.ReferenceEquals( currentType, typeof( UndoableBase ) ) ) );
        UndoChangesComplete( );
      }
    }

    [EditorBrowsable( EditorBrowsableState.Advanced )]
    protected virtual void AcceptChangesComplete( ) { }

    [EditorBrowsable( EditorBrowsableState.Never )]
    protected internal void AcceptChanges( ) {
      if ( this.EditLevel > 0 ) {
        stateStack.Pop( );
        Type currentType = this.GetType( );
        FieldInfo[ ] fields;

        do {
          fields = currentType.GetFields( BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public );
          foreach ( FieldInfo field in fields ) {
            if ( field.DeclaringType == currentType ) {
              if ( !NotUndoableField( field ) ) {
                if ( typeof( UndoableBase ).IsAssignableFrom( field.FieldType ) ) {
                  object value = field.GetValue( this );
                  if ( value != null ) {
                    ( ( UndoableBase ) value ).AcceptChanges( );
                  }
                }
              }
            }
          }
          currentType = currentType.BaseType;
        } while ( !( object.ReferenceEquals( currentType, typeof( UndoableBase ) ) ) );
      }
      AcceptChangesComplete( );
    }


    private static bool NotUndoableField( FieldInfo field ) {
      return Attribute.IsDefined( field, typeof( NotUndoableAttribute ) );
    }

    private static string GetFieldName( FieldInfo field ) {
      return field.DeclaringType.Name + "!" + field.Name;
    }

  }
}
